package com.feature50.clarity.table;

import org.apache.log4j.Logger;

import javax.swing.table.AbstractTableModel;
import javax.swing.JTable;
import javax.swing.event.TableModelListener;
import javax.swing.event.TableModelEvent;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Arrays;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;

import com.feature50.util.ReflectionUtils;
import com.feature50.util.ArrayUtils;
import com.feature50.util.StringUtils;
import com.feature50.clarity.ReflectionException;

public class ClarityTableModel extends AbstractTableModel {
    private static final Logger logger = Logger.getLogger(ClarityTableModel.class);

    private List objects;
    private ColumnDescriptor[] columnDescriptors;
    private String tableConfiguration;

    public ClarityTableModel(Object[] objects, String configurationData) {
        this(new ArrayList(Arrays.asList(objects)), configurationData);
    }

    public ClarityTableModel(List objects, String configurationData) {
        setObjects(objects, configurationData);
    }

    public void setObjects(List objects) {
        this.objects = (objects == null) ? new ArrayList() : new ArrayList(objects);

        fireTableDataChanged();
    }

    public void setObjects(List objects, String tableConfiguration) {
        this.objects = (objects == null) ? new ArrayList() : new ArrayList(objects);

        this.tableConfiguration = tableConfiguration;
        try {
            this.columnDescriptors = parseTableConfiguration();
        } catch (ParseException e) {
            throw new IllegalArgumentException("Table configuration invalid", e);
        }

        fireTableStructureChanged();
    }

    private void parseException(int index) throws ParseException {
        throw new ParseException("Table configuration malformed", index);
    }

    private ColumnDescriptor[] parseTableConfiguration() throws ParseException {
        List<ColumnDescriptor> descriptors = new ArrayList<ColumnDescriptor>();

        // having an instance associated with the table can help if indexed properties are mapped to the table
        Object instance = null;
        if (!ArrayUtils.isNullOrEmpty(objects)) {
            instance = objects.get(0);
        }

        // if the table configuration is null, create a default table configuration
        if (StringUtils.isNullOrEmpty(tableConfiguration)) {
            if (instance == null) throw new IllegalStateException("Neither an instance nor a configuration string was provided; at least one must be provided to construct a ClarityTableModel instance");
            tableConfiguration = createDefaultTableConfiguration(instance);
        }

        // create ColumnDescriptors for each column
        int startIndex = 0;
        int parens = 0;
        boolean bracket = false;
        for (int i = 1; i <= tableConfiguration.length(); i++) {
            char c = ',';
            if (i < tableConfiguration.length()) c = tableConfiguration.charAt(i);
            
            if (c == '[') {
                if (bracket) parseException(i);
                bracket = true;
            } else if (c == ']') {
                if (!bracket) parseException(i);
                bracket = false;
            } else if (c == '(') {
                parens++;
            } else if (c == ')') {
                if (parens <= 0) parseException(i);
                parens--;
            } else if (c == ',') {
                if ((!bracket) && (parens == 0)) {
                    String field = tableConfiguration.substring(startIndex, i).trim();
                    ValuePath[] valuePaths = ValuePath.getValuePaths(instance, field);
                    for (int j = 0; j < valuePaths.length; j++) {
                        descriptors.add(new ColumnDescriptor(valuePaths[j]));
                    }
                    startIndex = i + 1;
                }
            }
        }

        return descriptors.toArray(new ColumnDescriptor[0]);
    }

    public List getList() {
        return objects;
    }

    private String createDefaultTableConfiguration(Object instance) {
        List<String> properties = new ArrayList<String>();

        Method[] methods = ReflectionUtils.getGetterMethods(instance.getClass());
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            properties.add(ReflectionUtils.getPropertyFromEtter(method));
        }

        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < properties.size(); i++) {
            String field = properties.get(i);
            if (field != null) {
                if (sb.length() > 0) sb.append(", ");
                sb.append(field);
            }
        }

        return sb.toString();
    }

    public int getRowCount() {
        return objects.size();
    }

    public int getColumnCount() {
        return columnDescriptors.length;
    }

    public Object getValueAt(int row, int column) {
        return columnDescriptors[column].getGetter().getValue(objects.get(row));
    }

    public String getColumnName(int i) {
        return columnDescriptors[i].getName();
    }

    public boolean isCellEditable(int row, int column) {
        return (columnDescriptors[column].getSetter() != null);
    }

    public Class getColumnClass(int column) {
        Class clazz = columnDescriptors[column].getColumnClass();
        if (clazz == null) {
            if (getList().size() > 0) {
                Object instance = getList().get(0);
                setDescriptorColumnClass(columnDescriptors[column], instance);
            }
            clazz = columnDescriptors[column].getColumnClass();
        }
        return clazz;
    }

    public void setColumnClass(int column, Class clazz) {
        columnDescriptors[column].setColumnClass(clazz);
    }

    public void setValueAt(Object value, int row, int column) {
        try {
            columnDescriptors[column].getSetter().setValue(objects.get(row), value);
            fireTableCellUpdated(row, column);
        } catch (ReflectionException e) {
            logger.error("Couldn't update table cell", e);
        }
    }

    public Class getCellClassAt(int row, int column) {
        return getColumnClass(column);
    }

    public void fireTableRowsInserted(Object object) {
        int index = objects.indexOf(object);
        fireTableRowsInserted(index, index);
    }

    public ColumnDescriptor[] getColumnDescriptors() {
        return columnDescriptors;
    }

    public void fireTableChanged(TableModelEvent e) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();

        List<TableModelListener> list = new ArrayList<TableModelListener>();

        for (int i = listeners.length-2; i>=0; i-=2) {
            if (listeners[i]==TableModelListener.class) {
                list.add(((TableModelListener) listeners[i+1]));
            }
        }

        Collections.sort(list, new Comparator() {
            public int compare(Object o1, Object o2) {
                if ((o1 instanceof JTable) && (o2 instanceof JTable)) return 0;
                if (o1 instanceof JTable) return -1;
                if (o2 instanceof JTable) return 1;
                return 0;
            }
        });

        for (int i = 0; i < list.size(); i++) {
            TableModelListener tableModelListener = list.get(i);
            tableModelListener.tableChanged(e);
        }
    }

    private void setDescriptorColumnClass(ColumnDescriptor descriptor, Object instance) {
        try {
            Object fieldInstance = descriptor.getPath().getFieldInstance(instance);
            Method method = ReflectionUtils.getGetterMethod(fieldInstance.getClass(), descriptor.getPath().getField());
            descriptor.setColumnClass(method.getReturnType());
        } catch (Exception e) {
            throw new IllegalStateException(String.format("An instance in the model of type '%1$s' doesn't match the column configuration described by '%2$s'", instance.getClass(), descriptor.getPath().getDescription()), e);
        }
    }
}